Repository navigation
fix(objectql,metadata-protocol)!: a stored row bound to a package lives in that package's own registry slot, so each address serves its own row in every order (ADR-0150 D3/D4) - #33513
Conversation
…es in that package's own registry slot (ADR-0150 D3/D4) The registry kept one bare slot per name for a stored row, so the row saved or hydrated last took it. Each row bound to a package (its own, or its .custom pair's by the pair-row rule) of a type the registry reads by package now registers in that package's overlay slot, beside the code artifact's composite entry, which it never overwrites. - each package's exact read and registry item answer its own row in every registration, save and row order, before and after a restart, on the writer and on its peers; - a pair address of a name held only as stored rows answers its base's row; - the read naming no package of a package's row beside its pair's undeclared row serves the package's own row in both orders; - the read naming no package of an action two packages hold only as stored rows refuses 409 naming both after a restart, as every other type already did (the narrowing); - a bundle member two packages hold only as stored rows is no clash, as when one slot held one of them. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn
…ngine-double ledger Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn
📓 Docs Drift CheckThis PR changes 3 package(s): 36 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fd04e4b7fbb4da7048b3f0ec2955ea834760171e && git checkout fd04e4b7fbb4da7048b3f0ec2955ea834760171e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e40cd1e28dd79e60e7f4be0db06f071f3f962574 8d85d2b1afb2d6e89d6c28ee0f3aa64c9b427585 && git checkout -B drift-repro e40cd1e28dd79e60e7f4be0db06f071f3f962574 && git merge --no-ff 8d85d2b1afb2d6e89d6c28ee0f3aa64c9b427585
node scripts/docs-audit/affected-docs.mjs --json e40cd1e28dd79e60e7f4be0db06f071f3f962574
|
|
domain:server#5 · os-elon-musk · session_016WV4cy1y7UNVbT2QAdmgdn Request: review · area E1 · head 8d85d2b · 2026-11-14T23:16Z |
|
Contract-review-claim: session_016WV4cwxS1ZpSu2hAMRcqbt · head 8d85d2b · 2026-11-14T23:16Z review#1 (E1) takes this head. |
Contract reviewServed-tier: Face: schema (a ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
#33131 (asked by the author): this PR closes that card's remaining carried face (comment 6870666736, the both-overridden registry item), and the flipped pin proves it. Two items are still open on the card (comment 6878816309): nit N1 of record 6878787461 (unmeasured), and the Implemented-by: VERDICT: PASS |
Requested by Elon · project thread
Part of #33127 (its main case and the per-package overlay slot; the card stays open for its unpinned heal items: Heal 1, Heal 2 and S1)
Fixes #32900
Fixes #32663
Fixes #32662
Refs #32692
Clause-②: yes (narrowing)
Metadata-protocol: semantic
Skills-docs: none —
skills/**andcontent/docs/**teach no reading of a name two packages each store a row of (grepped?package=,.custom,409: the hits are the pair's save door, a flow's secret refusal and a permission grant), so no taught sentence moves; the read naming no package keeps its409and its?package=remedy.维护者速读
.custom配对包的行按配对规则归到它的基础包),不覆盖代码工件的条目。于是每个包的精确地址、注册表条目、配对地址都返回自己的那一行,不随保存顺序、加载顺序或重启变化。?package=返回出厂项而它自己的存储行没人读(metadata-protocol: after a restart, two stored rows of one name that each name a different holder leave only the last one loaded, for every package-slotted type (tool, webhook, …) #33127);写入方和对端副本的注册表条目只剩最后写的那一行(metadata-protocol: the writer's registry keeps one bare slot per stored name, so after one package saves a name two packages store, the other's exact address answers nothing (ADR-0150 D4) #32663);配对地址重启前 404、重启后一个 200 一个 409(metadata-protocol: after a reboot, the pair address X.custom of a name held only by stored rows of two enabled packages answers by hydration order (one 200, the other 409 naming one side) (ADR-0150 D3, red line 2) #32662);模块的 de-DE 语言成员跟着行顺序走(metadata-protocol: the registry's bare slot keeps only the row hydrated last when a pair row and a package-less row of the same member are both stored #32900)。packages/objectql/src/registry.ts的registerItem(新storedRowPackage选项与槽位键)、overlaySlotBinding末尾新分支、isBundleAddition的codeMembersOf;再看protocol-registry-hydration.ts的takesPackageOverlaySlot与restoreArtifactRegistryView的槽位清理。pnpm --filter @objectstack/objectql exec vitest run --project local src/webhook-stored-row-boot-hydration.test.ts src/protocol-cluster-signal-package.test.ts -t "33127|32663|32900",两种注册顺序、两种行顺序全部通过。Clause-②: yes (narrowing),受管面),然后席位落地。What changes
SchemaRegistrykept ONE bare slot per name for a stored row (registerItemwith no package), so the row saved or hydrated last took it, beside the code artifact's compositepkg:nameentry. Every symptom in this set is that one slot.The slot model (
packages/objectql/src/registry.ts).registerItem(type, item, keyField, packageId?, options?)takesoptions.storedRowPackage. Without apackageId, a row with a stored-row package registers under a package overlay slot key,MARK + package + MARK + bareKey, withMARKthe NUL character written as an escape in source. It never overwrites the bare key or a composite entry.bareSlotEntriesOfreads those slots after the bare entries, so every reader that judged the bare slot (overlaySlotBinding,packageScopedHoldersOf,getPackageScopedItem,boundServedActions,unregisterItem) sees each row once, bound by the same rule as before.rankedForPackageorders a package's bound rows as the stored-row read serves them: its pair's declared override, its own row, the package-less row, any other.orderedOverlayTierorders the overlay tier ofgetItemby holder (the caller's package first, then composite order, then id), sogetItemno longer follows row order.overlaySlotBinding: aBASE.customrow whose base holds the name only as its own stored row binds to the base, never becoming a second holder. That is the case one bare slot never held both rows of.isBundleAddition(…, codeMembersOf): a bundle member that two packages hold only as stored rows is no clash. One bare slot used to hold one of the two rows, so the base held the name in either order (protocol-bundle-member-base-address.test.tspins that answer, and it went red without this). A member either package ships in code still clashes.removePackageOverlaySlots(type, name, member, select)is new and public. It drops the selected packages' slots, for the delete's heal.Hydration (
packages/metadata-protocol/src/protocol-registry-hydration.ts).hydrateOverlayIntoRegistrypassesstoredRowPackagefor a row bound to a package of a type inPACKAGE_SLOTTED_STORE_TYPES, lessflowandpermission, which keep their own slot rules (takesPackageOverlaySlot).restoreArtifactRegistryViewretires the removed package's slot. Tier 1's bare-entry drop runs only when no slot was retired, and a heal with an unknown binding drops the slots of packages with no remaining row.Reads (
packages/metadata-protocol).admitExactQualifier(…, { storedRowBases }): a pair address of a name its bases hold only as stored rows is admitted at the base whose row is stored. Two such bases are refused naming both, as two registry bases are.readMetaItemasks for it and reads that base's content.storedRowHoldersRefusalserves the read naming no package at the one holder whose rows are its own and its pair's (metadata-protocol: after a reboot, the pair address X.custom of a name held only by stored rows of two enabled packages answers by hydration order (one 200, the other 409 naming one side) (ADR-0150 D3, red line 2) #32662 scope note b). It used to fall through to the first-stored row.Moved answers (FROM
maind4fbb4d → TO), per door:GET /meta/TYPE/NAME?package=ID, two shippers each with a stored row (tool, webhook, agent, skill), after a restartgetPackageScopedItem, writer and peer, after alpha then beta saved (action, tool, email_template)[null, beta](mirror in the other order)[alpha, beta]GET /meta/TYPE/NAME?package=ID.custom, name held only as stored rows (dashboard, tool, action), before a restartGET /meta/TYPE/NAME(and/published), a package's row beside its own pair's undeclared rowGET /meta/action/NAME, two packages hold it only as stored rows, after a restartRESOURCE_CONFLICTnaming both (the narrowing)getPackageScopedItem('email_template', welcome, X, 'de-DE'), X's pair row beside a package-less de-DE rowgetPackageScopedItem('action', …), both shippers with a declared override (#33131's carried face)ADR-0087: the semantic entry
stored-row-own-package-slot(packages/spec/src/migrations/entries/semantic/18.stored-row-own-package-slot.ts), markeradr-0087: registered stored-row-own-package-slotin.changeset/33127-stored-row-own-package-slot.md.Tests
Head
8d85d2b1af. Pins, each in every registration, save and row order, with CONTROLs (one holder, one row, a package-less row: unchanged). Each pin went in as a top-leveldescribein the file that already holds its harness.packages/objectql/src/webhook-stored-row-boot-hydration.test.ts:[#33127]covers tool, webhook, agent and skill: both holders ship the name, each stores a row. It checks the registry slot, the exact read, and that the read naming no package answers409 RESOURCE_CONFLICTnaming both.[#32900]checks X's de-DE slot in both row orders.packages/objectql/src/protocol-cluster-signal-package.test.ts,[#32663]: action, tool and email_template, writer and peer, both save orders.packages/runtime/src/domains/meta-disabled-holder-served-stack.test.ts,[#33127, #32662], throughHttpDispatcheron a booted kernel over a sqlite file, before and after a reboot:?package=per holder;?package=ID.custom;/published.The
[#32607 M1]test now also pins the base row on all three faces in both orders.Moved pins:
protocol-flow-custom-override.test.ts[#33131]: the carried registry face moved to its TO.protocol.register-item-call-sites.test.ts: the ledger records the newregisterItemcall shape.scripts/engine-double-contract.pinned.json: ratcheted with--write.Local runs (under `scripts/pm/os-verify-lock.sh`)
Tests 144 passed (144); runtime served-stack,Tests 36 passed (36); metadata-protocol call sites,Tests 5 passed (5).TC_EXIT=0.pnpm exec turbo run test --concurrency=2 --continueover the 83 packages that depend on objectql, metadata-protocol or runtime, plus@objectstack/spec. Result:Tasks: 186 successful, 188 total. The two failed tasks:@object-ui/example-schema-catalog#testprintedsh: 1: vitest: not found. That example's install is missing on this box; nothing was measured (NOT MEASURED).@objectstack/dogfood#test(the full battery, no shard):Test Files 2 failed | 463 passed | 3 skipped (468),Tests 2 failed | 3930 passed | 42 skipped (3974). Both failures wereTest timed out in 5000mswhile the box was contended:bare-name-one-holder-ai-family[#31985]andpackage-commit-revert-path-package[#29414] control. Re-run alone under the lock (VERDICT command-exit 0):Test Files 2 passed (2),Tests 39 passed (39).pnpm lintpassed (LINT_EXIT=0).Ablations: one per negative pin, each restored with a clean whole tree
Each mutation went through
scripts/ablation-replace.mjs.metadata-protocolwas rebuilt andscripts/ablation-dist-preflight.mjsfound the marker indist/. After the restore it was rebuilt again and the preflight with--absentanswered exit 0.overlaySlotforced off (markerABLATION_33127_SLOT):27 failed | 117 passed (144). The red tests are exactly the[#33127]boot pins (16),[#32663](6),[#33131]'s moved pin (4) and[#32900]pair-first (1).2 failed, the two#33127door pins.readMetaItem'sstoredRowBasesoff: runtime4 failed, the four#32662pair-address pins. objectql stayed green.storedRowHoldersRefusal's one-holder arm off (markerABLATION_33127_PLAINB): runtime3 failed, the two#32662 (b)pins andM1.isBundleAddition's row-only exemption off: objectql1 failed,[#28802] with a dependent's member at the same locale, the base's address still serves the pair's.Acceptance notes
protocol-flow-custom-override.test.tsmoved to the TO, and its title no longer says carried. That card stays open for its seat to judge.dashboardis still converged by name in the registry (metadata-protocol: the writer's registry keeps one bare slot per stored name, so after one package saves a name two packages store, the other's exact address answers nothing (ADR-0150 D4) #32663 residual (a), awaiting a ruling). The metadata-protocol: a cluster peer's metadata convergence re-reads and heals a removed row with no package, so a slotted type's peer can restore another package's item (ADR-0150 D4) #32451 CONTROL stays as it was.getMetaItemLayered) at a pair address is unchanged.registry.tsbelong to area E2b. Its open PRs (fix(objectql): judge an authored action artifact-shipped by each holder's owner key, not by bare name #33271, fix(runtime)!: the action route answers by who holds the name on the route, and the environment address answers its own item #33274) touch neitherregistry.tsnor any file here; fix(objectql): judge an authored action artifact-shipped by each holder's owner key, not by bare name #33271 reads composite keys inplugin.ts, which this change leaves as they were.Verification
pnpm check:adr-0087-registration:1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered stored-row-own-package-slot.node scripts/pm/dispatch-gates.mjs --commands, 102 families on this diff) were run locally.check:dual-build-cjs-loadsasks for a whole-repopnpm buildfirst, so it is NOT MEASURED here and left to CI.check:engine-double-contractandcheck:objectql-double-limitwere red on the first pass because of a new unboundedfinddouble. After the fix they are exit 0.check-nul-bytes: OK. The overlay slot mark is written as an escape, never a raw byte.packages/objectql/src/registry.ts: none (fix(objectql): judge an authored action artifact-shipped by each holder's owner key, not by bare name #33271 and fix(runtime)!: the action route answers by who holds the name on the route, and the environment address answers its own item #33274 were checked file by file).pnpm --filter @objectstack/objectql exec vitest run --project local src/webhook-stored-row-boot-hydration.test.ts -t 33127.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn